package com.gege.outbound;

import com.gege.filter.HeaderHttpRequestFilter;
import com.gege.filter.HttpRequestFilter;
import com.gege.router.HttpEndpointRouter;
import com.gege.router.RandomHttpEndpointRouter;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

/**
 * @author gechenpeng
 * @date 2021-04-02
 */
public class HttpOutboundHandler {

    private List<String> backendUrls;

    private HttpRequestFilter httpRequestFilter = new HeaderHttpRequestFilter();

    private HttpEndpointRouter router = new RandomHttpEndpointRouter();

    private ExecutorService executorService;

    private CloseableHttpClient httpClient;

    public HttpOutboundHandler(List<String> backends) {
        this.backendUrls = backends.stream().map(this::formatUrl).collect(Collectors.toList());

        int cores = Runtime.getRuntime().availableProcessors();
        long keepAliveTime = 1000;
        int queueSize = 2048;
        this.executorService = new ThreadPoolExecutor(cores, cores,
                keepAliveTime, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(queueSize),
                new NamedThreadFactory("executorService"),
                new ThreadPoolExecutor.CallerRunsPolicy());

        this.httpClient = HttpClients.createDefault();
    }

    public void handle(FullHttpRequest fullHttpRequest, ChannelHandlerContext ctx) {
        this.httpRequestFilter.filter(fullHttpRequest, ctx);
        String backendUrl =this.router.route(this.backendUrls);

        String method = fullHttpRequest.method().name();
        ByteBuf byteBuf = fullHttpRequest.content();
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        HttpHeaders httpHeaders = fullHttpRequest.headers();

        Map<String, String> headerMap = new HashMap<>(httpHeaders.size());
        Iterator<Map.Entry<String, String>> iterator = httpHeaders.iteratorAsString();
        while (iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            headerMap.put(entry.getKey(), entry.getValue());
        }

        Request request = Request.builder()
                .method(method)
                .url(backendUrl + fullHttpRequest.uri())
                .headers(headerMap)
                .content(new String(bytes))
                .build();

        this.executorService.execute(() -> fetch(request, ctx));
    }

    private void fetch(Request request, ChannelHandlerContext ctx) {
        HttpGet httpGet = new HttpGet(request.getUrl());

        FullHttpResponse response = null;
        try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) {
            HttpEntity entity = httpResponse.getEntity();
            byte[] body = EntityUtils.toByteArray(entity);
            EntityUtils.consume(entity);

            response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(body));
            HttpHeaders httpHeaders = response.headers();
            httpHeaders.set("Content-Type", "application/json");
            httpHeaders.setInt("Content-Length",
                    Integer.parseInt(httpResponse.getFirstHeader("Content-Length").getValue()));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            ctx.writeAndFlush(null == response ?
                    new DefaultFullHttpResponse(HTTP_1_1, NO_CONTENT) : response)
                    .addListener(ChannelFutureListener.CLOSE);
        }
    }



    private String formatUrl(String backend) {
        return backend.endsWith("/") ? backend.substring(0, backend.length() - 1) : backend;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    private static class Request {

        private String method;

        private String url;

        private Map<String, String> headers;

        private String content;
    }
}
